package org.xx.armory.objectparser.impl;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;
import org.xx.armory.commons.PathInputStream;
import org.xx.armory.objectparser.LineInfo;
import org.xx.armory.objectparser.ParsedObject;
import org.xx.armory.objectparser.ParsedObjectBuilder;
import org.xx.armory.objectparser.ParserException;
import org.xx.armory.objectparser.SourceFileParser;

import javax.xml.XMLConstants;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Paths;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;

import static org.xx.armory.commons.SysUtils.openInputStream;
import static org.xx.armory.commons.Validators.notBlank;

/**
 * XML源文件解析器。
 *
 * @author Haart
 */
public abstract class XMLFileParser<T extends ParsedObject>
        implements SourceFileParser<T> {
    private final Deque<String> sourceFilePaths;
    private final SAXParserFactory parserFactory;
    private final Deque<ParsedObjectBuilder<?>> objects;
    private final Logger logger = LoggerFactory.getLogger(XMLFileParser.class);

    /**
     * 构造XML源文件解析器。
     */
    public XMLFileParser() {
        this.sourceFilePaths = new ArrayDeque<>();
        this.objects = new ArrayDeque<>();

        // Create SAX parser factory.
        final SAXParserFactory factory = SAXParserFactory.newInstance();
        factory.setNamespaceAware(true);
        factory.setValidating(false);

        this.parserFactory = factory;
    }

    /**
     * 设置文件架构定义，此方法只可以在子类的构造函数中调用。
     *
     * @param schemaFileName
     *         架构定义文件名。
     */
    protected final void setSchema(
            String schemaFileName
    ) {
        try (final InputStream xsdStream = openInputStream(schemaFileName)) {
            final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);

            final Schema schema = schemaFactory.newSchema(new StreamSource(xsdStream));
            this.parserFactory.setSchema(schema);
        } catch (Exception ex) {
            logger.warn("cannot read schema", ex);
        }
    }

    /**
     * 获取所有解析中的文件栈。
     * <p>
     * <p>
     * 因为XML源文件可以包含别的源文件, 所以解析过程中会出现栈, 最先处理的文件在栈底。
     * </p>
     *
     * @return 所有解析的文件栈。
     */
    private Deque<String> getSourceFilePaths() {
        return sourceFilePaths;
    }


    /**
     * 获取XML元素处理器。
     *
     * @param c
     *         用于存放解析得到的元素的集合。
     * @return XML元素处理器。
     */
    protected abstract ElementHandler getHandler(final Collection<? super T> c);

    /**
     * 从解析结果栈顶获取最近的解析结果。
     *
     * @return 最近的解析结果。
     */
    protected final ParsedObjectBuilder<?> peek() {
        return this.objects.peek();
    }

    /**
     * 从解析结果栈顶弹出最近的解析结果。
     *
     * @return 最近的解析结果。
     */
    protected final ParsedObjectBuilder<?> pop() {
        return this.objects.pop();
    }

    /**
     * 向解析结果栈顶压入解析结果。
     *
     * @param o
     *         解析结果。
     */
    protected final void push(
            ParsedObjectBuilder<?> o
    ) {
        this.objects.push(o);
    }

    /**
     * 从解析文件栈中获取当前处理的文件。
     *
     * @return 当前处理的文件。
     */
    private String getCurrentFilePath() {
        return sourceFilePaths.peek();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void parse(
            String fileName,
            Collection<? super T> c
    )
            throws IOException {
        fileName = notBlank(fileName, "fileName").trim();

        final String sourceFilePath;
        if (!getSourceFilePaths().isEmpty()) {
            final String lastFilePath = getSourceFilePaths().peek();
            sourceFilePath = Paths.get(lastFilePath).resolveSibling(fileName).toAbsolutePath().toString();
        } else {
            sourceFilePath = Paths.get(fileName).toAbsolutePath().toString();
        }

        if (getSourceFilePaths().contains(sourceFilePath)) {
            throw new ParserException(String.format("duplicated file \"%s\".", sourceFilePath));
        }

        getSourceFilePaths().push(sourceFilePath);

        try (final InputStream aFileStream = new BufferedInputStream(new FileInputStream(sourceFilePath))) {
            logger.trace("parsing file \"{}\"...", sourceFilePath);

            parseStream(aFileStream, c);

            logger.trace("parsed file \"{}\".", sourceFilePath);
        }

        getSourceFilePaths().pop();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final void parse(
            InputStream stream,
            Collection<? super T> c
    )
            throws IOException {
        final String sourceFilePath;

        if (stream instanceof PathInputStream) {
            sourceFilePath = ((PathInputStream) stream).getPath();
        } else {
            sourceFilePath = "???";
        }

        getSourceFilePaths().push(sourceFilePath);

        logger.trace("parsing file \"{}\"...", sourceFilePath);

        parseStream(stream, c);

        logger.trace("parsed file \"{}\".", sourceFilePath);

        getSourceFilePaths().pop();
    }

    /**
     * 解析指定的输入流，并将解析结果保存到指定的集合。
     *
     * @param stream
     *         待解析的输入流。
     * @param c
     *         保存解析结果的集合。
     * @throws IOException
     *         如果读取输入流时出现错误。
     */
    protected void parseStream(
            InputStream stream,
            Collection<? super T> c
    )
            throws IOException {

        try {
            final SAXParser parser = parserFactory.newSAXParser();
            final ElementHandler handler = getHandler(c);
            parser.parse(new BufferedInputStream(stream), new DefaultHandler() {
                private Locator locator;

                private LineInfo getLineInfo() {
                    return new LineInfo(getCurrentFilePath(), this.locator.getColumnNumber(), this.locator.getColumnNumber());
                }

                @Override
                public void setDocumentLocator(
                        Locator locator
                ) {
                    // Save the locator.
                    // So that it can be used later for line tracking when
                    // traversing nodes.
                    this.locator = locator;
                }

                @Override
                public void startElement(
                        String uri,
                        String localName,
                        String qName,
                        Attributes attributes
                )
                        throws SAXException {
                    handler.startElement(getLineInfo(), localName, attributes);
                }

                @Override
                public void endElement(
                        String uri,
                        String localName,
                        String qName
                )
                        throws SAXException {
                    handler.endElement(getLineInfo(), localName);
                }

                @Override
                public void characters(
                        char[] ch,
                        int start,
                        int length
                )
                        throws SAXException {
                    handler.text(getLineInfo(), new String(ch, start, length));
                }

                @Override
                public void warning(SAXParseException ex)
                        throws SAXException {
                    // warnings.add(ex);
                    // TODO: 记录警告信息。
                }

                @Override
                public void error(SAXParseException ex)
                        throws SAXException {
                    throw ex;
                }

                @Override
                public void fatalError(SAXParseException ex)
                        throws SAXException {
                    throw ex;
                }
            });
        } catch (SAXParseException ex) {
            String filePath = getCurrentFilePath();
            throw new ParserException(new LineInfo(filePath, ex.getLineNumber(), ex.getColumnNumber()), ex);
        } catch (Exception ex) {
            throw new ParserException(ex);
        }
    }

    /**
     * 解析XML元素的处理器。
     *
     * @author Haart
     */
    protected interface ElementHandler {
        /**
         * 遇到XML元素开始标记时执行此方法。
         *
         * @param lineInfo
         *         源文件的位置信息。
         * @param name
         *         XML元素名。
         * @param attributes
         *         XML元素的所有属性。
         */
        void startElement(
                LineInfo lineInfo,
                String name,
                Attributes attributes
        );

        /**
         * 遇到XML元素的内部文本时执行此方法。
         *
         * @param lineInfo
         *         源文件的位置信息。
         * @param text
         *         XML元素的内部文本。
         */
        default void text(
                LineInfo lineInfo,
                String text
        ) {
        }

        /**
         * 遇到XML元素的内部文本时执行此方法。
         *
         * @param lineInfo
         *         源文件的位置信息。
         * @param name
         *         XML元素名。
         */
        void endElement(
                LineInfo lineInfo,
                String name
        );
    }

}
